package com.yxc.easyexceldemo.utils;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.excel.read.listener.ReadListener;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileNotFoundException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * EasyExcel工具类，参考<a href="https://easyexcel.opensource.alibaba.com/docs/current">EasyExcel官网</a>
 * @author: YXC
 * @time: 2022/11/15 15:05
 **/
@Slf4j
public class EasyExcelUtils {

    public static final String SHEET_NAME = "Sheet1";

    public static final Integer SHEET_NO = 0;

    /**
     * 读取少量数据直接到内存当中，数据量建议不超过1w，否则会耗费大量的内存
     *
     * @author YXC
     * @date 2022/11/15 15:10
     * @param file: excel文件
     * @param clazz: 对应的class类
     * @return java.util.List<T>
    **/
    public static <T> List<T> readLessData(File file, Class<T> clazz) {
        checkFile(file);
        LessDataListener lessDataListener = new LessDataListener();
        EasyExcel.read(file, lessDataListener).head(clazz).sheet().headRowNumber(1).doRead();
        return (List<T>) lessDataListener.getList();
    }

    public static <T> List<T> readLessData(String filePath, Class<T> clazz) {
        return readLessData(new File(filePath), clazz);
    }

    /**
     * 读取excel的表头，单行表头
     * 如果excel数据比较多，或许会读一遍，还没有测试
     *
     * @author YXC
     * @date 2022/11/16 11:25
     * @param file: excel文件
     * @return java.util.List<java.lang.String>
    **/
    public static List<String> readHead(File file) {
        checkFile(file);
        List<String> headList = new ArrayList<>();
        EasyExcel.read(file, new ReadHeadListener(headList)).sheet().doRead();
        return headList;
    }

    public static List<String> readHead(String filePath) {
        return readHead(new File(filePath));
    }

    /**
     * 读取大数据量的excel，分批返回
     *
     * @author YXC
     * @date 2022/11/16 16:44
     * @param file: excel文件
     * @param clazz: 文件对应的类
     * @param bathSize: 多少条返回一次，如果小于0，则抛出异常
     * @param consumer: 回调，参数为数据列表
     * @return void
    **/
    public static <T> void readBigData(File file, Class<T> clazz, Integer bathSize, Consumer<List<T>> consumer) {
        checkFile(file);
        checkBathSize(bathSize);

        List<T> dataList = Collections.synchronizedList(new ArrayList<>());
        EasyExcel.read(file, clazz, new ReadListener() {
            @Override
            public void invoke(Object data, AnalysisContext context) {
                if (dataList.size() >= bathSize) {
                    consumer.accept(dataList);
                    dataList.clear();
                }
                dataList.add((T) data);
            }

            @Override
            public void doAfterAllAnalysed(AnalysisContext context) {
                consumer.accept(dataList);
            }
        });
    }

    public static <T> void readBigData(String filePath, Class<T> clazz, Integer bathSize, Consumer<List<T>> consumer) {
        readBigData(new File(filePath), clazz, bathSize, consumer);
    }

    public static <T> void readBigData(File file, Class<T> clazz, Consumer<List<T>> consumer) {
        readBigData(file, clazz, 100, consumer);
    }

    public static <T> void readBigData(String filePath, Class<T> clazz, Consumer<List<T>> consumer) {
        readBigData(filePath, clazz, 100, consumer);
    }

    /**
     * 导出少量的数据
     *
     * @author YXC
     * @date 2022/11/16 17:29
     * @param file: 要写的文件
     * @param dataList: 数据列表
     * @param clazz: 数据对应class
     * @param heads: 自定义表头，如果自定义，必须与去重（bean对象字段数 - 排除列 + 包含列）相匹配
     * @param includeColumnFieldNames: 要包含的字段  可以是字段英文名，也可以是ExcelProperty注解的value值  和要排除的字段只能传一个  如果两个都穿，以要包含的字段为准
     * @param excludeColumnFieldNames: 要排除的字段  可以是字段英文名，也可以是ExcelProperty注解的value值  和要包含的字段只能传一个
     * @param sheetName: sheet表名字
     * @param sheetNo: sheet表编号，从0开始
     * @return void
    **/
    public static <T> void writeLessData(File file, List<T> dataList, Class<T> clazz, List<String> heads, Set<String> includeColumnFieldNames, Set<String> excludeColumnFieldNames, String sheetName, Integer sheetNo) {
        checkDataList(dataList);
        Set<String> includeFields = getIncludeFields(clazz, includeColumnFieldNames, excludeColumnFieldNames);
        createFile(file);
        List<List<String>> headList = checkHeads(heads, includeFields);
        sheetName = checkSheetName(sheetName);
        sheetNo = checkSheetNo(sheetNo);
        EasyExcel
                .write(file, clazz)
                .sheet(sheetNo, sheetName)
                .head(headList)
                .includeColumnFieldNames(includeFields)
                .doWrite(dataList);
    }

    public static <T> void writeLessData(String filePath, List<T> dataList, Class<T> clazz, List<String> heads, Set<String> includeColumnFieldNames, Set<String> excludeColumnFieldNames, String sheetName, Integer sheetNo) {
        writeLessData(new File(filePath), dataList, clazz, heads, includeColumnFieldNames, excludeColumnFieldNames, sheetName, sheetNo);
    }

    public static <T> void writeLessData(String filePath, List<T> dataList, Class<T> clazz, Set<String> includeColumnFieldNames, Set<String> excludeColumnFieldNames, String sheetName, Integer sheetNo) {
        writeLessData(new File(filePath), dataList, clazz, null, includeColumnFieldNames, excludeColumnFieldNames,
                sheetName, sheetNo);
    }

    public static <T> void writeLessData(File file, List<T> dataList, Class<T> clazz) {
        writeLessData(file, dataList, clazz, null, null, null, null, null);
    }

    public static <T> void writeLessData(String filePath, List<T> dataList, Class<T> clazz) {
        writeLessData(new File(filePath), dataList, clazz, null, null, null, null, null);
    }

    /**
     * 创建excel写对象，大数据量对象可以使用ExcelWriter去写
     *
     * <code>
     *     try (ExcelWriter writer = buildExcelWriter(file, A.class, heads, includeColumnFieldNames, excludeColumnFieldNames)) {
     *         WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
     *         for (int i = 0; i < 100; i++) {
     *             writer.write(data(), writeSheet);
     *         }
     *     }
     * </code>
     *
     * @author YXC
     * @date 2022/11/17 10:44
     * @param file: 写出的文件
     * @param clazz:
     * @param heads: 表头
     * @param includeColumnFieldNames: 包含列
     * @param excludeColumnFieldNames: 排除列
     * @return com.alibaba.excel.ExcelWriter
    **/
    public static <T> ExcelWriter buildExcelWriter(File file, Class<T> clazz, List<String> heads, Set<String> includeColumnFieldNames, Set<String> excludeColumnFieldNames) {
        Set<String> includeFields = getIncludeFields(clazz, includeColumnFieldNames, excludeColumnFieldNames);
        createFile(file);
        List<List<String>> headList = checkHeads(heads, includeFields);
        return EasyExcel
                .write(file, clazz)
                .head(headList)
                .includeColumnFieldNames(includeFields)
                .build();
    }

    public static <T> ExcelWriter buildExcelWriter(File file, Class<T> clazz) {
        return buildExcelWriter(file, clazz, null, null, null);
    }

    public static <T> ExcelWriter buildExcelWriter(String filePath, Class<T> clazz) {
        return buildExcelWriter(new File(filePath), clazz, null, null, null);
    }

    @SneakyThrows
    private static void checkBathSize(Integer bathSize) {
        if (bathSize <= 0) {
            throw new IllegalArgumentException("bathSize必须大于0");
        }
    }

    @SneakyThrows
    private static void checkFile(File file) {
        if (!file.exists()) {
            throw new FileNotFoundException("文件" + file.getPath() + "不存在");
        }
    }

    /**
     * 检查sheet编号，若为空，则赋值SHEET_NO
     *
     * @author YXC
     * @date 2022/11/17 10:21
     * @param sheetNo:
     * @return java.lang.Integer
    **/
    private static Integer checkSheetNo(Integer sheetNo) {
        return sheetNo == null ? SHEET_NO : sheetNo;
    }

    /**
     * 检查sheet名字，若为空，则赋值SHEET_NAME
     *
     * @author YXC
     * @date 2022/11/17 10:21
     * @param sheetName:
     * @return java.lang.String
    **/
    private static String checkSheetName(String sheetName) {
        return sheetName == null || sheetName.length() == 0 ? SHEET_NAME : sheetName;
    }

    /**
     * 检查表头和要导出的字段是否匹配
     *
     * @author YXC
     * @date 2022/11/17 10:15
     * @param heads :
     * @param includeFields :
     * @return void
    **/
    private static List<List<String>> checkHeads(List<String> heads, Set<String> includeFields) {
        if (heads == null) {
            return null;
        }
        if (heads.size() != includeFields.size()) {
            throw new IllegalArgumentException("自定义表头与导出字段数量不匹配");
        }
        List<List<String>> headList = new ArrayList<>();
        for (String head : heads) {
            List<String> tmp = new ArrayList<>();
            tmp.add(head);
            headList.add(tmp);
        }
        return headList;
    }

    @SneakyThrows
    private static void createFile(File file) {
        if (file == null) {
            throw new IllegalArgumentException("文件不能为空");
        }
        if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();
        }
        if (!file.exists()) {
            file.createNewFile();
        }
    }

    /**
     * 检查写文件的相关参数，并返回要包含的字段
     *
     * @author YXC
     * @date 2022/11/17 10:06
     * @param clazz:
     * @param includeColumnFieldNames:
     * @param excludeColumnFieldNames:
     * @return java.util.List<java.lang.String>
    **/
    private static <T> Set<String> getIncludeFields(Class<T> clazz, Set<String> includeColumnFieldNames, Set<String> excludeColumnFieldNames) {
        Field[] fields = clazz.getFields();
        // 全部要导出的字段
        List<Field> fieldList = Arrays.stream(fields).filter(field -> field.getAnnotation(ExcelIgnore.class) == null).collect(Collectors.toList());
        List<ExcelField> excelFieldList = fieldList.stream().map(field -> {
            ExcelField excelField = new ExcelField();
            String name = field.getName();
            excelField.setName(name);
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
            if (excelProperty != null) {
                excelField.setFieldName(excelProperty.value()[0]);
                excelField.setIndex(excelProperty.index());
            }
            return excelField;
        }).collect(Collectors.toList());
        // 如果includeColumnFiledNames不为空，直接返回含有的
        if (includeColumnFieldNames != null && !includeColumnFieldNames.isEmpty()) {
            Set<String> includeList = new HashSet<>();
            for (ExcelField excelField : excelFieldList) {
                if (includeColumnFieldNames.contains(excelField.getName()) || includeColumnFieldNames.contains(excelField.getFieldName())) {
                    includeList.add(excelField.getName());
                }
            }
            return includeList;
        }
        if (excludeColumnFieldNames != null && !excludeColumnFieldNames.isEmpty()) {
            Iterator<ExcelField> iterator = excelFieldList.iterator();
            while (iterator.hasNext()) {
                ExcelField next = iterator.next();
                // 包含中文或者英文，都去掉
                if (excludeColumnFieldNames.contains(next.getFieldName()) || excludeColumnFieldNames.contains(next.getName())) {
                    iterator.remove();
                }
            }
        }
        return excelFieldList.stream().map(ExcelField::getName).collect(Collectors.toSet());
    }

    private static <T> void checkDataList(List<T> dataList) {
        if (dataList == null) {
            throw new IllegalArgumentException("数据不能为null");
        }
    }

    @Data
    static class ExcelField {
        String fieldName;
        String name;
        Integer index;
    }

    /**
     * 读取excel表头的listener
     *
     * @author YXC
     * @date 2022/11/16 11:31
    **/
    static class ReadHeadListener extends AnalysisEventListener<Object> {

        List<String> headList;

        public ReadHeadListener(List<String> headList) {
            this.headList = headList;
        }

        @Override
        public void onException(Exception exception, AnalysisContext context) {
            log.error("解析失败，但是继续解析下一行: {}", exception.getMessage());
            // 如果是某一个单元格的转换异常 能获取到具体行号
            // 如果要获取头的信息 配合invokeHeadMap使用
            if (exception instanceof ExcelDataConvertException) {
                ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
                log.error("第{}行，第{}列解析异常，数据为:{}",
                        excelDataConvertException.getRowIndex(),
                        excelDataConvertException.getColumnIndex(),
                        excelDataConvertException.getCellData());
            }
        }

        @Override
        public void invoke(Object data, AnalysisContext context) {}

        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {}

        @Override
        public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
            Collection<String> heads = headMap.values();
            if (!heads.isEmpty()) {
                this.headList.addAll(heads);
            }
        }
    }

    /**
     * 读取少量数据使用的listener，会全部加载到内存
     *
     * @author YXC
     * @date 2022/11/16 16:46
    **/
    static class LessDataListener extends AnalysisEventListener<Object> {

        private List<Object> list = new ArrayList<>();

        @Override
        public void onException(Exception exception, AnalysisContext context) throws ExcelDataConvertException {
            log.error("读取excel失败");
            if (exception instanceof ExcelDataConvertException) {
                ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
                // 建议使用全局异常捕获去捕获
                throw excelDataConvertException;
            }
        }

        @Override
        public void invoke(Object data, AnalysisContext context) {
            list.add(data);
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {}

        public List<Object> getList() {
            return list;
        }

        public void setList(List<Object> list) {
            this.list = list;
        }
    }

}
